/*
 * Copyright 1999-2018 Alibaba Group Holding Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.nacos.controller;

import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.auth.annotation.Secured;
import com.alibaba.nacos.auth.common.ActionTypes;
import com.alibaba.nacos.auth.common.AuthConfigs;
import com.alibaba.nacos.auth.common.AuthSystemTypes;
import com.alibaba.nacos.auth.exception.AccessException;
import com.alibaba.nacos.common.model.RestResult;
import com.alibaba.nacos.common.model.RestResultUtils;
import com.alibaba.nacos.common.utils.JacksonUtils;
import com.alibaba.nacos.config.server.auth.RoleInfo;
import com.alibaba.nacos.config.server.model.User;
import com.alibaba.nacos.config.server.utils.RequestUtil;
import com.alibaba.nacos.security.nacos.JwtTokenManager;
import com.alibaba.nacos.security.nacos.NacosAuthConfig;
import com.alibaba.nacos.security.nacos.NacosAuthManager;
import com.alibaba.nacos.security.nacos.roles.NacosRoleServiceImpl;
import com.alibaba.nacos.security.nacos.users.NacosUser;
import com.alibaba.nacos.security.nacos.users.NacosUserDetailsServiceImpl;
import com.alibaba.nacos.utils.PasswordEncoderUtil;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;

/**
 * User related methods entry.
 *
 * @author wfnuser
 * @author nkorange
 */
@RestController("user")
@RequestMapping({ "/v1/auth", "/v1/auth/users" })
public class UserController {

	@Autowired
	private JwtTokenManager jwtTokenManager;

	@Autowired
	private AuthenticationManager authenticationManager;

	@Autowired
	private NacosUserDetailsServiceImpl userDetailsService;

	@Autowired
	private NacosRoleServiceImpl roleService;

	@Autowired
	private AuthConfigs authConfigs;

	@Autowired
	private NacosAuthManager authManager;

	/**
	 * Create a new user.
	 * @param username username
	 * @param password password
	 * @return ok if create succeed
	 * @throws IllegalArgumentException if user already exist
	 * @since 1.2.0
	 */
	@Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE)
	@PostMapping
	public Object createUser(@RequestParam String username, @RequestParam String password) {

		User user = userDetailsService.getUserFromDatabase(username);
		if (user != null) {
			throw new IllegalArgumentException("user '" + username + "' already exist!");
		}
		userDetailsService.createUser(username, PasswordEncoderUtil.encode(password));
		return RestResultUtils.success("create user ok!");
	}

	/**
	 * Delete an existed user.
	 * @param username username of user
	 * @return ok if deleted succeed, keep silent if user not exist
	 * @since 1.2.0
	 */
	@DeleteMapping
	@Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE)
	public Object deleteUser(@RequestParam String username) {
		List<RoleInfo> roleInfoList = roleService.getRoles(username);
		if (roleInfoList != null) {
			for (RoleInfo roleInfo : roleInfoList) {
				if (roleInfo.getRole().equals(NacosRoleServiceImpl.GLOBAL_ADMIN_ROLE)) {
					throw new IllegalArgumentException("cannot delete admin: " + username);
				}
			}
		}
		userDetailsService.deleteUser(username);
		return RestResultUtils.success("delete user ok!");
	}

	/**
	 * Update an user.
	 * @param username username of user
	 * @param newPassword new password of user
	 * @param response http response
	 * @param request http request
	 * @return ok if update succeed
	 * @throws IllegalArgumentException if user not exist or oldPassword is incorrect
	 * @since 1.2.0
	 */
	@PutMapping
	@Secured(resource = NacosAuthConfig.UPDATE_PASSWORD_ENTRY_POINT, action = ActionTypes.WRITE)
	public Object updateUser(@RequestParam String username, @RequestParam String newPassword,
			HttpServletResponse response, HttpServletRequest request) throws IOException {
		// admin or same user
		if (!hasPermission(username, request)) {
			response.sendError(HttpServletResponse.SC_FORBIDDEN, "authorization failed!");
		}

		User user = userDetailsService.getUserFromDatabase(username);
		if (user == null) {
			throw new IllegalArgumentException("user " + username + " not exist!");
		}

		userDetailsService.updateUserPassword(username, PasswordEncoderUtil.encode(newPassword));

		return RestResultUtils.success("update user ok!");
	}

	private boolean hasPermission(String username, HttpServletRequest request) {
		if (!authConfigs.isAuthEnabled()) {
			return true;
		}
		if (Objects.isNull(request.getAttribute(RequestUtil.NACOS_USER_KEY))) {
			return false;
		}

		NacosUser user = (NacosUser) request.getAttribute(RequestUtil.NACOS_USER_KEY);
		// admin
		if (user.isGlobalAdmin()) {
			return true;
		}
		// same user
		return user.getUserName().equals(username);
	}

	/**
	 * Get paged users.
	 * @param pageNo number index of page
	 * @param pageSize size of page
	 * @return A collection of users, empty set if no user is found
	 * @since 1.2.0
	 */
	@GetMapping
	@Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.READ)
	public Object getUsers(@RequestParam int pageNo, @RequestParam int pageSize) {
		return userDetailsService.getUsersFromDatabase(pageNo, pageSize);
	}

	/**
	 * Login to Nacos
	 *
	 * <p>
	 * This methods uses username and password to require a new token.
	 * @param username username of user
	 * @param password password
	 * @param response http response
	 * @param request http request
	 * @return new token of the user
	 * @throws AccessException if user info is incorrect
	 */
	@PostMapping("/login")
	public Object login(@RequestParam String username, @RequestParam String password, HttpServletResponse response,
			HttpServletRequest request) throws AccessException {

		if (AuthSystemTypes.NACOS.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())
				|| AuthSystemTypes.LDAP.name().equalsIgnoreCase(authConfigs.getNacosAuthSystemType())) {
			NacosUser user = (NacosUser) authManager.login(request);

			response.addHeader(NacosAuthConfig.AUTHORIZATION_HEADER, NacosAuthConfig.TOKEN_PREFIX + user.getToken());

			ObjectNode result = JacksonUtils.createEmptyJsonNode();
			result.put(Constants.ACCESS_TOKEN, user.getToken());
			result.put(Constants.TOKEN_TTL, authConfigs.getTokenValidityInSeconds());
			result.put(Constants.GLOBAL_ADMIN, user.isGlobalAdmin());
			result.put(Constants.USERNAME, user.getUserName());
			return result;
		}

		// create Authentication class through username and password, the implement class
		// is UsernamePasswordAuthenticationToken
		UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,
				password);

		try {
			// use the method authenticate of AuthenticationManager(default implement is
			// ProviderManager) to valid Authentication
			Authentication authentication = authenticationManager.authenticate(authenticationToken);
			// bind SecurityContext to Authentication
			SecurityContextHolder.getContext().setAuthentication(authentication);
			// generate Token
			String token = jwtTokenManager.createToken(authentication);
			// write Token to Http header
			response.addHeader(NacosAuthConfig.AUTHORIZATION_HEADER, "Bearer " + token);
			return RestResultUtils.success("Bearer " + token);
		}
		catch (BadCredentialsException authentication) {
			return RestResultUtils.failed(HttpStatus.UNAUTHORIZED.value(), null, "Login failed");
		}
	}

	/**
	 * Update password.
	 * @param oldPassword old password
	 * @param newPassword new password
	 * @return Code 200 if update successfully, Code 401 if old password invalid,
	 * otherwise 500
	 */
	@PutMapping("/password")
	@Deprecated
	public RestResult<String> updatePassword(@RequestParam(value = "oldPassword") String oldPassword,
			@RequestParam(value = "newPassword") String newPassword) {
		Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		String username = ((UserDetails) principal).getUsername();
		User user = userDetailsService.getUserFromDatabase(username);
		String password = user.getPassword();

		// TODO: throw out more fine grained exceptions
		try {
			if (PasswordEncoderUtil.matches(oldPassword, password)) {
				userDetailsService.updateUserPassword(username, PasswordEncoderUtil.encode(newPassword));
				return RestResultUtils.success("Update password success");
			}
			return RestResultUtils.failed(HttpStatus.UNAUTHORIZED.value(), "Old password is invalid");
		}
		catch (Exception e) {
			return RestResultUtils.failed(HttpStatus.INTERNAL_SERVER_ERROR.value(), "Update userpassword failed");
		}
	}

	/**
	 * Fuzzy matching username.
	 * @param username username
	 * @return Matched username
	 */
	@GetMapping("/search")
	@Secured(resource = NacosAuthConfig.CONSOLE_RESOURCE_NAME_PREFIX + "users", action = ActionTypes.WRITE)
	public List<String> searchUsersLikeUsername(@RequestParam String username) {
		return userDetailsService.findUserLikeUsername(username);
	}

}
